﻿using System;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Security.Principal;
using System.Text;
using System.Data;
using System.Data.Sql;
using System.Data.SqlClient;
using System.Web.Configuration;



namespace HttpModuleAccessControl
{

    public class FirstClass : IHttpModule
    {
        private string DBConnectionString = Config.DBConnectionString;
        // private static bool _debug = true;

        // for persistent object testing only 
        // private static int _num = 0;

        // Persistent static objects for RBAC
        public static Users _FirstClassUsers;
        protected static Roles _FirstClassRoles;
        protected static Permissions _FirstClassPermissions;

        private static object _initSyncRoot = new object();

        // By using AuthenticationSection, we can retrieve the properties in the
        // section of authentication under system.web in web.config
        private static AuthenticationSection _AutheSection = null;

        // must be implemented
        public void Dispose()
        { }

        // Init - the first method that will be invoked in HttpModule must be implemented
        // Init method will be invoked once when httpModule is not instantiated
        /// <summary>
        /// 1. Load Users ( user with roles)  from database
        /// 2. Load Roles ( role with permissions)  from database
        /// 3. Load authentication method in web.config  from web.config
        /// </summary>
        /// <param name="HttpApp"></param>
        public void Init(HttpApplication HttpApp)
        {
            Log.Write("HttpModule Init begins ! ");

            // init RBAC information
            // 1. Load users ( future : including user hierarchy)
            Log.Write("HttpModule Init method, ready to load users");
            _FirstClassUsers = new Users(true);

            // 2. Load roles ( future : including roles hierarchy)
            Log.Write("HttpModule Init method, ready to load roles");
            _FirstClassRoles = new Roles(true);

            // 3. Load Permission
            Log.Write("HttpModule Init method, ready to load permissions");
            _FirstClassPermissions = new Permissions(true);
            
            #region TestOnly - dump Users, Roles, Permissions data
            //test only, print all data in Users, Roles, Permissions static objects
            if (false)  // change false to true to dump the access control data to log file
            {
                Log.DumpRBAC(_FirstClassUsers, _FirstClassRoles, _FirstClassPermissions);
            }
            #endregion

            // For system first piority check.
            HttpApp.BeginRequest += new EventHandler(httpmodBeginRequest);

            // For authentication and authorization, detemine which authentication option goes which functionality
            System.Configuration.Configuration configuration = System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("/WebApplication1");
            _AutheSection = (AuthenticationSection)configuration.GetSection("system.web/authentication");
            AuthenticationMode _AutheMode = _AutheSection.Mode;
            
            //if (_debug) Log.Write(_AutheMode.ToString());

            switch (_AutheMode)
            { 
                case AuthenticationMode.Forms :
                    Log.Write("Go to AuthenticationMode.Forms");
                    HttpApp.AuthenticateRequest += new EventHandler(FormsAuthenticateRequest);
                    HttpApp.AuthorizeRequest += new EventHandler(FormsAuthorizedRequest);
                    break;
                
                // future : for windows domain case, refer to Active Directory programming later.
                case AuthenticationMode.Windows :

                    break;

            }

            // Jan 2009 : v0.2 - Adding query notification functionality
            this.RunQueryNotification();

            Log.Write("HttpModule Init ends ! ");
        }

        // v0.2 - query notification 
        void RunQueryNotification()
        {
            Log.Write("Running Query Notification for Users data..");

            // clear old data for query notification for sys.dm_qn_subscriptions and queue
            Database.ClearQueryNotificationData();

            try
            {             
                // setup sqlconnection, sqlcommand and SqlNotificationRequest
                SqlConnection conn = new SqlConnection(Config.DBConnectionString);
                conn.Open();

                SqlCommand commandUsers = new SqlCommand("select userid, password, username, usertitle, userdescription from dbo.Users", conn);
                SqlNotificationRequest notif_Users = new SqlNotificationRequest();
                notif_Users.UserData = Guid.NewGuid().ToString();
                notif_Users.Options = "Service=" + Config.ServiceName();
                notif_Users.Timeout = Int32.MaxValue;
                commandUsers.Notification = notif_Users;
                commandUsers.ExecuteNonQuery();

                SqlCommand commandRoles = new SqlCommand("select roleid, rolename, parentid from dbo.Roles", conn);
                SqlNotificationRequest notif_Roles = new SqlNotificationRequest();
                notif_Roles.UserData = Guid.NewGuid().ToString();
                notif_Roles.Options = "Service=" + Config.ServiceName();
                notif_Roles.Timeout = Int32.MaxValue;
                commandRoles.Notification = notif_Roles;
                commandRoles.ExecuteNonQuery();

                SqlCommand commandPermissions = new SqlCommand("select PermissionID,PermissionName,ParentID,WebPageName from dbo.Permissions", conn);
                SqlNotificationRequest notif_Permissions = new SqlNotificationRequest();
                notif_Permissions.UserData = Guid.NewGuid().ToString();
                notif_Permissions.Options = "Service=" + Config.ServiceName();
                notif_Permissions.Timeout = Int32.MaxValue;
                commandPermissions.Notification = notif_Permissions;
                commandPermissions.ExecuteNonQuery();

                SqlCommand commandUR = new SqlCommand("select UserID,RoleID from dbo.UserinRole", conn);
                SqlNotificationRequest notif_UR = new SqlNotificationRequest();
                notif_UR.UserData = Guid.NewGuid().ToString();
                notif_UR.Options = "Service=" + Config.ServiceName();
                notif_UR.Timeout = Int32.MaxValue;
                commandUR.Notification = notif_UR;
                commandUR.ExecuteNonQuery();

                SqlCommand commandRP = new SqlCommand("select  RoleID,PermissionID from dbo.RoleinPermission", conn);
                SqlNotificationRequest notif_RP = new SqlNotificationRequest();
                notif_RP.UserData = Guid.NewGuid().ToString();
                notif_RP.Options = "Service=" + Config.ServiceName();
                notif_RP.Timeout = Int32.MaxValue;
                commandRP.Notification = notif_RP;
                commandRP.ExecuteNonQuery();

                // setup callback for the message from queue in Service Broker of SQL Server
                SqlCommand commandCallback = new SqlCommand("WAITFOR (Receive * from " + Config.QueueName() + ")", conn);
                commandCallback.CommandTimeout = int.MaxValue;

                AsyncCallback callBack = new AsyncCallback(this.OnChangeComplete);
                IAsyncResult asynResult = commandCallback.BeginExecuteReader(callBack, commandCallback);

                Log.Write("Query Notification is setup completely.");
                if (asynResult.IsCompleted == true)
                {
                    //waitInProgress = true;

                }
            }
            catch (Exception ex)
            {
                Log.Write("Frist class, RunQueryNotification_Users method, Exception= " + ex.Message);

            }



        }

 
        // v0.2 - Query notification callback function
        void OnChangeComplete(IAsyncResult asynResult)
        {
            // re-load _FirstClassUsers, _FirstClassRoles, _FirstClassPermissions
            Log.Write("Query notification is activated. Re-load RBAC information.");
            try
            {
                _FirstClassUsers = new Users(true);
                _FirstClassRoles = new Roles(true);
                _FirstClassPermissions = new Permissions(true);
                this.RunQueryNotification();
            }
            catch (Exception ex)
            {
                Log.Write("Re-load RBAC information error! Exception= " + ex.Message);
            }

            #region TestOnly - dump Users, Roles, Permissions data
            //test only, print all data in Users, Roles, Permissions static objects
            if (false)  // change false to true to dump the access control data to log file
            {
                Log.DumpRBAC(_FirstClassUsers, _FirstClassRoles, _FirstClassPermissions);
            }
            #endregion

        }

    

        /// <summary>
        /// It runs when a http connection appears. 
        /// For system first piority check.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void httpmodBeginRequest(object sender, EventArgs e)
        {

            // Check existance of solution-level variables, e.g. Environment variables, configuration file and so on.
            Log.Write("HttpModule BeginRequest starts ==================");

            Log.Write("HttpModule BeginRequest ends ====================");

        }

        /// <summary>
        /// Check FormsAuthentication Ticket. If ticket is empty, then user in HTTPContext is null.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void FormsAuthenticateRequest(object sender, EventArgs e)
        {
            Log.Write("HttpModule AuthenticateRequest starts ====================");

            HttpApplication application = (HttpApplication)sender;
            HttpRequest httpReq = application.Context.Request;
            HttpResponse httpResp = application.Context.Response;

            // There has authentication cookie. Get the authentication cookie value.            
            FormsAuthenticationTicket authTicket = GetTicket(application.Context);

            // if cookie is empty, then whole process should be stopped.
            // if cookie is not empty, then 1. if username exists in Users object, go to AuthorizeRequest function.
            //                              2. if username doesn't exist in Users object, go to AccessDenied page.
            if (authTicket == null)
            {
                application.Context.User = null;
                // should be redirected to login.aspx or do nothing here because authTicket is null
                // Setting of FormsAuthentication will do the redirection work, so just *return* here.
                Log.Write("HttpModule AuthenticateRequest exits because of FormsAuthenticationTicket is null.");
                return;
            }

            try
            {
                if (authTicket.Expired)
                {
                    FormsAuthentication.SignOut();
                    StringBuilder sb = new StringBuilder(httpReq.Path);
                    sb.Append("?");

                    // There will always be something in the query string
                    sb.Append("SessionTimeout=").Append("1");

                    string CurQueryStringValue;
                    foreach (string QueryStringKey in httpReq.QueryString.Keys)
                    {
                        switch (QueryStringKey)
                        {
                            // Skip the time out
                            case "SessionTimeout":
                                break;
                            default:
                                CurQueryStringValue = httpReq.QueryString[QueryStringKey];
                                if (null != CurQueryStringValue)
                                    sb.Append("&").Append(QueryStringKey).Append("=").Append(CurQueryStringValue);
                                break;
                        }
                    }
                    httpResp.Redirect(sb.ToString(), true);
                    Log.Write("HttpModule AuthenticateRequest exits because of FormsAuthenticationTicket Expired is true.");
                    return;
                }
                else
                {
                    RefreshTicketIfOld(application.Context, authTicket);
                }
            }
            catch
            {
                application.Context.User = null;
                Log.Write("HttpModule AuthenticateRequest exits because of HttpApplication.Context.User is null.");
                return;
            }

            Log.Write("HttpModule AuthenticateRequest ends ======================");

        }

        /// <summary>
        /// 1. Get username from HttpRequest, if none, go to login page
        /// 2. Authenticate username with Users object(static object)
        ///    if username exists in Users object, go to AuthorizeRequest
        ///    if none, go to AccessDenied page.
        ///    
        /// 1. Get page name  from HttpRequest
        /// 2. Get Permissions based on user's roles  from RolePermission static object
        /// 3. Merge all permissions
        /// 4. Search page name in RolePermissions static object
        ///    if found --> granted
        ///    if not found --> go to AccessDenied page.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void FormsAuthorizedRequest(object sender, EventArgs e)
        {
            string FunctionName = "FirstClass, FormsAuthorizedRequest method, ";

            Log.Write("HttpModule AuthorizedRequest starts ====================");

            HttpApplication application = (HttpApplication)sender;
            HttpRequest httpReq = application.Context.Request;
            HttpResponse httpResp = application.Context.Response;

            if (httpReq.IsAuthenticated == false)
            {
                Log.Write("HttpModule AuthorizedRequest exits because HttpRequest IsAuthenticated is false.");
                return;
            }
               

            // Get user ID from HttpApplication
            string userid = application.User.Identity.Name;

            // Get pagename from HttpContext
            string pagename = GetASPXName(HttpContext.Current.Request.RawUrl);
            // Pass when target page is not .ASPX file (no access control check)
            if (pagename == string.Empty)
            {
                Log.Write("HttpModule AuthorizedRequest exits because target page name is empty.");
                return;
            }

            // pagename pass rule
            // If target page is not in control, let it pass without access control, such as AccessDenied.aspx
            if (PagenamePassRule(pagename))
            {
                Log.Write("HttpModule AuthorizedRequest exits because target page name is satisfied the pass rule.");
                return;
            }
                
            
            // If username is empty, go to login page. (no access control check)
            // If username is not empty, check it with User static object
            if (userid == string.Empty)
            {
                Log.Write("HttpModule AuthorizedRequest exits because target page name is satisfied the pass rule.");
                return ;
            }
            else
            {
                // check whether the username exists in User object
                if (_FirstClassUsers.Contains(userid))
                {   // exist in User object
                    //Log.Write(FunctionName + ", User " + userid + " is in User object");
                    // do something for RBAC
                    try
                    {
                        /// 2. Get Permissions based on user's roles  from RolePermission static object                        
                        /// 3. Merge all permissions
                        
                        // find user index in _FirstClassUser, and get roleList by this index
                        int _userIndex = _FirstClassUsers.FindUserListIndex(userid);
                        // userid does exists in User object, so _index must be equal to or greater than 0
                        if (_userIndex >= 0)
                        {
                            //Log.Write("check point 2, user index=" + _userIndex.ToString());

                            ArrayList _roleList = _FirstClassUsers.Item(_userIndex).RoleList;
                            Permissions _userPers = new Permissions();

                            //Log.Write("check point 3");
                            foreach (object obj in _roleList)
                            {
                                int _roleid = int.Parse(obj.ToString());

                                //Log.Write("roleID= " + _roleid.ToString());

                                int _roleIndex = _FirstClassRoles.FindRoleListIndex(_roleid);
                                ArrayList _tempPermissionIDs = _FirstClassRoles.Item(_roleIndex).PermissionList;  
 
                                //Log.Write("role's permission count = " + _tempPermissionIDs.Count.ToString());   

                                foreach (object o2 in _tempPermissionIDs)
                                {
                                    //Log.Write("add permission WebPageName = " + p.WebPageName);
                                    Permission p = _FirstClassPermissions.Element(Convert.ToInt32(o2.ToString()));                                                                       
                                    _userPers.Add(p);
                                }
                            }

                            /// 4. Search page name in RolePermissions static object
                            ///    if found --> granted
                            ///    if not found --> go to AccessDenied page.
                            ///    
                            //--> start here
                            bool _access = false;
                            //Log.Write("check point 4");

                            foreach (Permission p in _userPers)
                            {
                                //Log.Write("memory pagename=" + p.WebPageName.ToUpper().Trim() + ",Request pagename=" + pagename.ToUpper().Trim());

                                if (p.WebPageName.ToUpper().Trim() == pagename.ToUpper().Trim())
                                {
                                    Log.Write(FunctionName + ", User " + userid + " can access " + pagename);
                                    _access = true;
                                    break;
                                    // found, granted  (do nothing in this version)

                                    // future : put Writable property 
                                    //        create HttpContext Items for UserPagePermission info , Writable
                                    //        UserPagePermission upp = new UserPagePermission(HttpContext.Current.User.Identity.Name, _aspxName);
                                    //        HttpContext.Current.Items["UserPagePermission"] = upp;

                                }
 
                            }

                            //Log.Write("check point 5");
                            if (_access == false)
                            {
                                Log.Write(FunctionName + ", User " + userid + " cannot access " + pagename);
                                HttpContext.Current.Response.Redirect(@"~\AccessDenied.aspx");
                                //HttpContext.Current.RewritePath(@"~\AccessDenied.aspx");
                                //return;                           
                            }

                        }
                        else
                        {
                            Log.Write(FunctionName + ", User " + userid + " index value is less than 0.");
                            HttpContext.Current.Response.Redirect(@"~\AccessDenied.aspx");
                            //HttpContext.Current.RewritePath(@"~\AccessDenied.aspx");
                            //return;
                        }                                                                               

                    }
                    catch
                    {
                        application.Context.User = null;
                        //return;
                    }

                }
                else
                {   // not exist in User object
                    // go to access denied page
                    Log.Write(FunctionName + ", User " + userid +" is NOT in User object");
                    // set user in HTTP context as null. This helps AuthorizeRequest decide this as a un-authenticated connection.
                    HttpContext.Current.Response.Redirect(@"~\AccessDenied.aspx");
                    //HttpContext.Current.RewritePath(@"~\AccessDenied.aspx");
                    Log.Write(FunctionName + " User " + userid + " is redirected to AccessDenied.aspx");

                    //return;
                } 
            }

            Log.Write("HttpModule AuthorizedRequest ends ======================");
        }


        private FormsAuthenticationTicket GetTicket(HttpContext context)
        {
            string ErrorTitle = "FristClass class, GetTicket function = ";

            // There has authentication cookie.
            HttpRequest httpReq = context.Request;
            FormsAuthenticationTicket authTicket = null;

            try
            {
                // Extract the forms authentication cookie            
                HttpCookie authCookie = httpReq.Cookies[GetAuthenticationSection().Forms.Name];
                if (null == authCookie || String.IsNullOrEmpty(authCookie.Value))
                {
                    // There is no authentication cookie.
                    return null;
                }

                authTicket = FormsAuthentication.Decrypt(authCookie.Value);
                if (null == authTicket)
                {
                    // Cookie failed to decrypt.
                    Log.Write(ErrorTitle + " Cookie failed to decrypt.");
                }
            }
            catch (Exception err)
            {
                err.Data["RawUrl"] = httpReq.RawUrl;
                //err.Data["CookieRawData"] = authCookie.Value;
                err.Data["UrlReferrer"] = httpReq.UrlReferrer;
                Log.Write(ErrorTitle + " error= " + err.Message);
            }

            return authTicket;

        }

        /// <summary>
        ///  Web.Config system.web/authentication Configuration Section
        /// </summary>
        /// <returns></returns>
        public static AuthenticationSection GetAuthenticationSection()
        {
            if (_AutheSection == null)
            {
                lock (_initSyncRoot)
                {
                    if (_AutheSection == null)
                    {
                        _AutheSection = WebConfigurationManager.GetSection("system.web/authentication") as AuthenticationSection;
                    }
                }
            }
            return _AutheSection;
        }


        private void RefreshTicketIfOld(HttpContext context, FormsAuthenticationTicket tOld)
        {
            if ((DateTime.Now - tOld.IssueDate) > (tOld.Expiration - DateTime.Now))
            {
                DateTime refreshTime = DateTime.Now;

                FormsAuthenticationTicket newTicket = new FormsAuthenticationTicket(tOld.Version,
                    // RSU: §âTicketªºTimeout³]©wªº¸ò Session ¤@¼Ë,
                     tOld.Name, refreshTime, refreshTime.Add(GetAuthenticationSection().Forms.Timeout),
                     tOld.IsPersistent, tOld.UserData, tOld.CookiePath);


                //Db.NetBankUsersEntity.UserKeepAlive(new Guid(newTicket.Name),
                //    (int)AuthEventCode.Refresh, 0);

                string encryptedTicket = FormsAuthentication.Encrypt(newTicket);

                // Create a cookie and add the encrypted ticket to the
                // cookie as data.
                HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
                authCookie.Secure = FormsAuthentication.RequireSSL;

                // RSU: ¤£¯à assign Expires,¦]¬°¨S¦³Expiresªº®É­Ô,lifetime¬° browser scope,·í¦³­Èªº®É­Ô, ·|³QPersistent // authCookie.Expires = newTicket.Expiration;
                authCookie.HttpOnly = true;

                if (!string.IsNullOrEmpty(FormsAuthentication.FormsCookiePath))
                    authCookie.Path = FormsAuthentication.FormsCookiePath;

                if (!string.IsNullOrEmpty(FormsAuthentication.CookieDomain))
                    authCookie.Domain = FormsAuthentication.CookieDomain;

                // Add the cookie to the outgoing cookies collection.
                context.Response.Cookies.Add(authCookie);
            }
        }

        // future : define a structure to return folder name + file name
        private string GetASPXName(string URLString)
        {
            string answer = string.Empty;

            int a = URLString.IndexOf(".aspx");

            int index = 1;

            if (a > 0)
            {
                while (true)
                {

                    // future : add folder name result , not just filename
                    if (URLString.Substring(a - index, 1) == "/")
                    {
                        answer = URLString.Substring(a - index + 1, index - 1) + ".aspx";
                        break;
                    }
                    index++;

                    //just in case to avoid infinite loop
                    if (index > a) break;
                }
            }


            Log.Write("FirstClass, pagename = " + answer);
            return answer;
        }

        // future : read from a config file for page pass rule
        private bool PagenamePassRule(string Pagename)
        {
            bool _answer = false;

            // future : read rule from a config file or web.config
            if (Pagename.ToUpper().Trim() == "ACCESSDENIED.ASPX")
                _answer = true;


            return _answer;
        }

    }//end of class
}
